// TODO: Add support for multi-html-element FormElements (e.g. TimeElement)
// TODO: Add better support for getting/setting values for each type of element
// TODO: Add support for changing whether an element appears as required

var FORM_ID_PREFIX = 'form-';
var FORM_CONTAINER_SUFFIX = '-container';
var FORM_LABEL_SUFFIX = '-label';
var FORM_REVERT_TABLE_ID = FORM_ID_PREFIX + 'revert-table';
var FORM_EDIT_PARAM = 'e';
var FORM_REVERT_PARAM = 'r';

function FormSystem() {
  this.elements = {};
  this.versions = [];
  this.formElementId = null;
  this.refresh = this.refresh.bind(this);
}

FormSystem.prototype.load = function() {
  util.addEvent(window, 'resize', this.refresh);
};

FormSystem.prototype.setFormElementId = function(id) {
  this.formElementId = id;
};

FormSystem.prototype.addElement = function(element) {
  this.elements[element.name] = element;
};

FormSystem.prototype.getElement = function(elementName) {
  return this.elements[elementName];
};

FormSystem.prototype.refresh = function() {
  for (var i in this.elements) {
    this.elements[i].refresh();
  }
};

FormSystem.prototype.addVersion = function(versionInfo) {
  this.versions.push(versionInfo);
};

FormSystem.prototype.showRevertDialog = function() {
  var title = 'Revert To Previous Version';
  var dialog = new Dialog(title, this.revert.bind(this), null);
  dialog.contentHTML = this.getRevertTableHTML();
  dialog.saveButtonText = 'Revert';
  dialog.show();
}

FormSystem.prototype.revert = function() {
  var radios = $(FORM_REVERT_TABLE_ID).getElementsByTagName('input');
  for (var i = 0; i < radios.length; i++) {
    if (radios[i].checked) {
      var parameters = {};
      parameters[FORM_REVERT_PARAM] = radios[i].value;
      var url = util.buildUrl(window.location, parameters);
      window.location = url;
    }
  }
};

FormSystem.prototype.getRevertTableHTML = function() {
  var html = '<table id="' + FORM_REVERT_TABLE_ID +
      '" cellspacing="0" cellpadding="0">';
  html += '<tr><th></th><th>Edited By</th><th>Date</th></tr>';
  for (var i = 0; i < this.versions.length; i++) {
    var version = this.versions[i];
    var date = util.formatDate('m-d-Y h:i a', version.date);
    html += '<tr>';
    html += '<td><input type="radio" name="revert" value="' + version.id +
        '" /></td>';
    html += '<td>' + version.editedBy + '</td>';
    html += '<td>' + date + '</td>';
    html += '</tr>';
  }
  html += '</table>';
  return html;
};

var formSystem = new FormSystem();
util.addEvent(window, 'load', formSystem.load.bind(formSystem));

function FormElement(name, opt_valueElementId, opt_autoFocusElementId,
    opt_visibleElementIds) {
  Observer.call(this);
  Subject.call(this);

  this.name = name;
  this.mainElementId = FORM_ID_PREFIX + name;
  this.valueElementId = opt_valueElementId || FORM_ID_PREFIX + name;
  this.autoFocusElementId = opt_autoFocusElementId || FORM_ID_PREFIX + name;
  this.visibleElementIds = opt_visibleElementIds || [FORM_ID_PREFIX + name];

  this.containerId = FORM_ID_PREFIX + name + FORM_CONTAINER_SUFFIX;
  this.labelId = FORM_ID_PREFIX + name + FORM_LABEL_SUFFIX;
  this.hint = null;

  if ($(this.valueElementId)) {
    util.addEvent($(this.valueElementId), 'change', this.notify.bind(this));
  }
}
FormElement.inherits(Observer);
FormElement.inherits(Subject);

FormElement.prototype.setVisibility = function(visible) {
  if (visible) {
    $(this.containerId).style.display = '';
  } else {
    $(this.containerId).style.display = 'none';
  }
};

FormElement.prototype.setLabel = function(label) {
  $(this.labelId).innerHTML = label;
};

FormElement.prototype.setHint = function(hintText, hintMode) {
  if (this.hint) {
    this.hint.destruct();
  }
  if (hintMode == HINT_MODE_MANUAL) {
    this.hint = new FormElementHint(this.mainElementId, hintText, hintMode,
        this.getHintPosition.bind(this));
  } else if (hintMode == HINT_MODE_AUTO) {
    this.hint = new FormElementHint(this.mainElementId, hintText, hintMode, null,
        this.getHintFocusTarget.bind(this));
  } else {
    throw 'Unexpected hint mode';
  }
};

/*
  Since each form element positions their hint button a little differently,
  each one can override this function as necessary to make things look right.
*/
FormElement.prototype.getHintPosition = function() {
  /* Find the position of the right extreme of the element */
  var maxXPos = 0;
  for (var i = 0; i < this.visibleElementIds.length; i++) {
    var element = $(this.visibleElementIds[i]);
    var xPos = util.findPos(element)[0] + element.offsetWidth;
    if (xPos > maxXPos) {
      maxXPos = xPos;
    }
  }
  var position = util.findPos($(this.labelId));
  position[0] = maxXPos;
  return position;
};

FormElement.prototype.getHintFocusTarget = function() {
  return $(this.autoFocusElementId);
};

FormElement.prototype.setDisabled = function(disabled) {
  for (var i = 0; i < this.visibleElementIds.length; i++) {
    var element = $(this.visibleElementIds[i]);
    element.disabled = disabled;
  }
};

FormElement.prototype.getValue = function() {
  return $(this.valueElementId).value;
};

FormElement.prototype.setValue = function(value) {
  $(this.valueElementId).value = value;
};

FormElement.prototype.setAutoFocus = function(autofocus) {
  if (autofocus) {
    $(this.autoFocusElementId).focus();
  }
};

FormElement.prototype.refresh = function() {
  if (this.hint) {
    this.hint.refresh();
  }
};

function CheckboxElement(name) {
  FormElement.call(this, name);
}
CheckboxElement.inherits(FormElement);

CheckboxElement.prototype.checked = function() {
  return $(this.mainElementId).checked;
};

function DateElement(name) {
  FormElement.call(this, name);
  this.dateChooser = new DateChooser(this.mainElementId);
}
DateElement.inherits(FormElement);

function AutoCompleteElement(name) {
  FormElement.call(this, name);
  this.autoComplete = new AutoComplete(this.mainElementId);
}
AutoCompleteElement.inherits(FormElement);

AutoCompleteElement.prototype.setOptions = function(options) {
  this.autoComplete.setOptions(options);
};

var WYSIWYG_MODE_BASIC = 0;
var WYSIWYG_MODE_FULL = 1;

function WysiwygElement(name, stylesheet, mode) {
  FormElement.call(this, name);

  if (mode == WYSIWYG_MODE_BASIC) {
    var buttons1 = ['bold', 'italic', 'underline', 'strikethrough', '|',
        'justifyleft', 'justifycenter', 'justifyright', 'justifyfull', '|',
        'bullist', 'numlist', '|', 'undo', 'redo', '|', 'link', 'unlink', '|',
        'code'].join();
    var buttons2 ='';
    var buttons3 = '';
    var plugins = "inlinepopups";
  } else if (mode == WYSIWYG_MODE_FULL) {
    var buttons1 = ['bold', 'italic', 'underline', 'strikethrough', '|',
        'justifyleft', 'justifycenter', 'justifyright', 'justifyfull', '|',
        'forecolor', 'backcolor', '|', 'formatselect', 'fontselect',
        'fontsizeselect'].join();
    var buttons2 = ['cut', 'copy', 'paste', 'pastetext', 'pasteword', '|',
        'search', 'replace', '|', 'bullist', 'numlist', '|', 'outdent',
        'indent', '|', 'undo', 'redo', '|', 'link', 'unlink',
        'anchor', '|', 'image', '|', 'code', 'fullscreen'].join();
    var buttons3 = ['tablecontrols', '|', 'hr', 'removeformat', '|', 'sub',
        'sup', '|', 'charmap'].join();
    var plugins = "paste,table,searchreplace,inlinepopups,fullscreen";
  }

  this.startResize = this.startResize.bind(this);
  this.stopResize = this.stopResize.bind(this);

  /*
    Kinda bizarre. This is a function passed to tinyMCE.init which sets up an
    event to fire when a particular editor instance is done loading. When the
    event fires, we attach an event to the resize handle on the editor.
  */
  var startResize = this.startResize;
  var observeResize = function(editor) {
    editor.onInit.add(function(editor) {
      /* Check to see if we're resizing */
      if ($(editor.id + '_resize')) {
        util.addEvent($(editor.id + '_resize'), 'mousedown', startResize);
      }
    });
  };

  var tinyMCEInitObj = {
    mode : "exact",
    elements : this.mainElementId,
    plugins : plugins,
    theme : "advanced",
    theme_advanced_buttons1 : buttons1,
    theme_advanced_buttons2 : buttons2,
    theme_advanced_buttons3 : buttons3,
    theme_advanced_toolbar_location : "top",
    theme_advanced_toolbar_align : "left",
    theme_advanced_statusbar_location : "bottom",
    theme_advanced_resize_horizontal : false,
    theme_advanced_resizing : true,
    extended_valid_elements : "a[name|href|target|title|onclick],img[class|" +
        "src|alt|title|hspace|vspace|width|height|align|onmouseover|" +
        "onmouseout|name|style],hr[class|width|size|noshade],font[face|size|" +
        "color|style],span[class|align|style],style",
    relative_urls: false,
    convert_urls: false,
    content_css : stylesheet,
    setup : observeResize
  };
  tinyMCE.init(tinyMCEInitObj);
};
WysiwygElement.inherits(FormElement);

WysiwygElement.prototype.startResize = function() {
  util.addEvent(document, 'mouseup', this.stopResize);
  /* This is a bit ugly, would be better to have a custom event model */
  util.addEvent(document, 'mousemove', formSystem.refresh);
};

WysiwygElement.prototype.stopResize = function() {
  util.removeEvent(document, 'mouseup', this.stopResize);
  util.removeEvent(document, 'mousemove', formSystem.refresh);

  /* Resize one more time just to be sure */
  window.setTimeout(formSystem.refresh, 100);
};

/*
  Makes a best possible effort to determine the width of the element.
*/
WysiwygElement.prototype.getHintPosition = function() {
  var position = util.findPos($(this.labelId));
  var element = $(this.mainElementId + '_tbl');
  if (!element || element.style.display == 'none') {
    /* Can't get size, bail. */
    return false;
  }
  position[0] = util.findPos(element)[0] + element.offsetWidth;
  return position;
};

function ListElement(name, valueElementId, focusElementId, visibleElementIds) {
  FormElement.apply(this, arguments);

  this.values = $(valueElementId).value.split(',');

  this.addButtonId = this.mainElementId + '-add';
  this.entryBoxId = this.mainElementId + '-entry';
  this.selectBoxId = this.mainElementId + '-list';
  this.deleteButtonId = this.mainElementId + '-delete';
  this.upButtonId = this.mainElementId + '-up';
  this.downButtonId = this.mainElementId + '-down';

  this.setupEventHandlers();
}
ListElement.inherits(FormElement);

ListElement.prototype.addValue = function(value) {
  if (this.values.contains(value)) {
    return;
  }
  this.values.push(value);
  this.updateValueElement();
  this.updateListBox();
};

ListElement.prototype.removeValue = function(value) {
  this.values.remove(value);
  this.updateValueElement();
  this.updateListBox();
};

// TODO: Determine if the entry already exists!!!!!!

ListElement.prototype.updateValueElement = function() {
  $(this.valueElementId).value = this.values.join(',');
};

ListElement.prototype.updateListBox = function() {
  $(this.selectBoxId).options.length = 0;
  for (var i = 0; i < this.values.length; i++) {
    try {
      $(this.selectBoxId).add(new Option(this.values[i], this.values[i]), null);
    } catch (ex) {
      $(this.selectBoxId).add(new Option(this.values[i], this.values[i]),
          $(this.selectBoxId).options.length);
    }
  }
};

ListElement.prototype.setupEventHandlers = function() {
  $(this.addButtonId).onclick = this.addButtonClick.bind(this);
  $(this.deleteButtonId).onclick = this.deleteButtonClick.bind(this);
};

ListElement.prototype.addButtonClick = function() {
  /* Get the value from the text box, then clear the box */
  var newItem = $(this.entryBoxId).value;

  /* Add the value to the end of the list */
  if (newItem != '') {
    this.addValue(newItem);
  }
};

ListElement.prototype.deleteButtonClick = function() {
  var optionsCollection = $(this.selectBoxId).options;
  var optionsToRemove = []
  for (var i = 0; i < optionsCollection.length; i++) {
    if (optionsCollection[i].selected) {
      optionsToRemove.push(optionsCollection[i].value);
    }
  }
  for (var i = 0; i < optionsToRemove.length; i++) {
    this.removeValue(optionsToRemove[i]);
  }
};


/*
  ------------------------------------------------------------------------------
  Hints!
  ------------------------------------------------------------------------------
*/

var HINT_MODE_MANUAL = 0;
var HINT_MODE_AUTO = 1;
var HINT_DELAY = 1000;

/*
  If you specify an opt_mode of 'auto', you need to also specify
  opt_autoFocusElementId so the hint knows where to attach focus and blur events
*/
function FormElementHint(mainElementId, text, mode,
    opt_getPositionCallback, opt_getFocusTargetCallback) {

  this.text = text;
  this.mode = mode;
  this.getPositionCallback = opt_getPositionCallback;
  this.getFocusTargetCallback = opt_getFocusTargetCallback;

  this.containerId = mainElementId + '-hint-container';
  this.buttonId = mainElementId + '-hint-button';
  this.visible = false;
  this.opacity = 0;

  /* Prebound so we can attach and remove them as event handlers */
  this.showEventHandler = this.showWithDelay.bind(this);
  this.hideEventHandler = this.hide.bind(this);

  if (this.mode == HINT_MODE_MANUAL) {
    this.showButton();
  } else if (this.mode == HINT_MODE_AUTO) {
    this.setupEventHandlers();
  } else {
    throw 'Invalid hint mode specified';
  }
}

FormElementHint.prototype.showButton = function() {
  var delayDOMAppend = function() {
    this.distanceFromElement = 28;
    var button = document.createElement('div');
    button.id = this.buttonId;
    button.className = 'form-hint-button';
    button.title = 'Click to see the hint for this field';
    button.onclick = this.show.bind(this, false);
    this.positionButton(button);
    document.body.appendChild(button);
  };

  /* IE freaks out if you try to manipulate the DOM while the page is loading */
  if (document.readyState == 4) {
    delayDOMAppend.call(this);
  } else {
    util.addEvent(window, 'load', delayDOMAppend.bind(this));
  }
};

FormElementHint.prototype.positionButton = function(opt_button) {
  var button = opt_button || $(this.buttonId);
  var buttonPosition = this.getPositionCallback();
  if (buttonPosition) {
    button.style.left = buttonPosition[0] + 'px';
    button.style.top = buttonPosition[1] + 'px';
  }
};

FormElementHint.prototype.refresh = function() {
  if ($(this.buttonId)) {
    this.positionButton();
  }
};

FormElementHint.prototype.setupEventHandlers = function() {
  this.distanceFromElement = 8;
  var focusElement = this.getFocusElementCallback();
  util.addEvent(focusElement, 'focus', this.showEventHandler);
  util.addEvent(focusElement, 'blur', this.hideEventHandler);
};

FormElementHint.prototype.showWithDelay = function (delay) {
  this.timer = window.setTimeout(this.show.bind(this, false), HINT_DELAY);
};

/*
  Create the hintbox and shadow.
*/
FormElementHint.prototype.show = function (delay) {
  if (this.visible) {
    this.hide();
    return;
  }
  var hintContainer = this.buildHintContainer();
  document.body.appendChild(hintContainer);
  this.visible = true;

  (new FadeAnimation(this.containerId, 100, 300, 10)).animate();
};

FormElementHint.prototype.buildHintContainer = function() {
  var hintContainer = document.createElement('div');
  hintContainer.id = this.containerId;
  hintContainer.className = 'form-hint-container';
  /*
    Browser detection since you can't detect filters on nodes not yet added to
    the DOM
  */
  if (/msie/i.test(navigator.userAgent) && !window.opera) {
    /*
      Absurd hack for IE which can't handle the opacity filter on a relatively
      positioned div shifted outside its container.
    */
    hintContainer.style.border = '4px solid transparent';
  }

  var hintContent = document.createElement('div');
  hintContent.className = 'form-hint-content';
  hintContainer.appendChild(hintContent);

  var hintCloseLink = document.createElement('div');
  hintCloseLink.className = 'form-hint-closelink';
  hintCloseLink.onclick = this.hide.bind(this);
  hintContent.appendChild(hintCloseLink);

  var hintTitle = document.createElement('div');
  hintTitle.className = 'form-hint-title';
  hintTitle.innerHTML = 'Hint for this field:';
  hintContent.appendChild(hintTitle);

  hintContent.appendChild(document.createTextNode(this.text));

  var pos = this.getPositionCallback();

  hintContainer.style.left = (pos[0] + this.distanceFromElement) + 'px';
  hintContainer.style.top = pos[1] + 'px';
  util.setOpacity(hintContainer, 0);
  hintContainer.style.display = 'block';

  return hintContainer;
};

/*
  Hide the hint.
*/
FormElementHint.prototype.hide = function () {
  this.visible = false;
  if (this.timer) {
    clearTimeout(this.timer);
  }
  try {
    $(this.containerId).parentNode.removeChild($(this.containerId));
  } catch (e) {
    /* Do nothing. */
  }
};

FormElementHint.prototype.destruct = function() {
  if (this.mode == HINT_MODE_MANUAL) {
    $(this.buttonId).parentNode.removeChild($(this.buttonId));
  } else if (this.mode == HINT_MODE_AUTO) {
    var focusElement = this.getFocusElementCallback();
    util.removeEvent(focusElement, 'focus', this.showEventHandler);
    util.removeEvent(focusElement, 'blur', this.hideEventHandler);
  }
};

/*
  ------------------------------------------------------------------------------
  Revert to previous version dialog
  ------------------------------------------------------------------------------
*/

function VersionInfo(id, timestamp, editedBy) {
  this.id = id;
  this.date = new Date((parseInt(timestamp, 10) * 1000));
  this.editedBy = editedBy;
}
